﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Ionic.Zip;
using HtmlAgilityPack;
using Kooboo.HtmlFunc;
using Kooboo.HtmlFunc.Providers;
using Kooboo.CMS.SiteConverter.Models;
using Kooboo.CMS.SiteConverter.Parser;
using Kooboo.CMS.SiteConverter.Providers;

namespace Kooboo.CMS.SiteConverter
{
    public class ParserUtility
    {
        public const string CachedAnalyseResultsKey = "CachedAnalyseResults";
        public const string CurrentAnalyseResultKey = "CurrentAnalyseResult";
        public const string AllSchemasKey = "AllSchemas";

        private static List<IDirectoryMap> DirectoryMap;

        static ParserUtility()
        {
            // load directory map
            DirectoryMap = new List<IDirectoryMap>();
            if (File.Exists(ParserSetting.ResDirectoryPath))
            {
                var mapping = RuntimeKit.XmlDeserialize<List<DirectoryMap>>(ParserSetting.ResDirectoryPath);
                foreach (var map in mapping)
                    DirectoryMap.Add(map);
            }
            else
            {
                var stream = RuntimeKit.LoadEmbeddedResource("DirectoryMap.xml");
                var mapping = RuntimeKit.XmlDeserialize<List<DirectoryMap>>(stream);
                foreach (var map in mapping)
                    DirectoryMap.Add(map);
            }
        }

        public static IEnumerable<IAnalyseResult> AnalysePage(string fileName, string pageHtml)
        {
            return Kooboo.HtmlFunc.AnalysisKit.AnalysePage(fileName, pageHtml);
        }

        public static IParseResult ParsePage(IAnalyseResult analyseResult, IParserFactory parser)
        {
            var result = new ParseResult();
            parser.Context.Result = result;

            // parse order must from inner to outer
            foreach (var meta in analyseResult.Metas)
            {
                parser.MetaParser.Parse(meta);
            }

            var fileUrls = new List<Kooboo.CMS.SiteConverter.Models.FileUrl>();
            foreach (var img in analyseResult.ImgTags) { fileUrls.Add(new FileUrl() { Node = img.Node }); }
            foreach (var style in analyseResult.StyleTags) { if (style.IsLink) fileUrls.Add(new FileUrl() { Node = style.Node }); }
            foreach (var script in analyseResult.ScriptTags) { fileUrls.Add(new FileUrl() { Node = script.Node }); }
            foreach (var anchor in analyseResult.AnchorTags) { fileUrls.Add(new FileUrl() { Node = anchor.Node }); }
            foreach (var fielUrl in fileUrls)
            {
                parser.FileUrlParser.Parse(fielUrl);
            }

            foreach (var label in analyseResult.Labels)
            {
                parser.LabelParser.Parse(label);
            }

            foreach (var block in analyseResult.HtmlBlocks)
            {
                parser.HtmlBlockPartParser.Parse(block);
            }

            foreach (var menu in analyseResult.Menus)
            {
                parser.MenuParser.Parse(menu);
            }

            foreach (var list in analyseResult.Lists)
            {
                parser.ListParser.Parse(list);
            }

            foreach (var detail in analyseResult.Details)
            {
                parser.DetailParser.Parse(detail);
            }

            foreach (var view in analyseResult.ViewParts)
            {
                parser.ViewPartParser.Parse(view);
            }

            foreach (var htmlPos in analyseResult.HtmlParts)
            {
                parser.HtmlPartParser.Parse(htmlPos);
            }

            foreach (var layoutPos in analyseResult.Positions)
            {
                parser.PositionParser.Parse(layoutPos);
            }

            if (analyseResult.TitleTag != null)
                parser.Context.Items["TitleKey"] = analyseResult.TitleTag;
            parser.PageSettingParser.Parse(analyseResult.PageSetting);

            parser.LayoutParser.Parse(analyseResult.Layout);

            // ret
            return result;
        }

        public static IParseResult ParsePage(IAnalyseResult analyseResult, ParserContext context, ParserType parserType = ParserSetting.DefaultParserType)
        {
            if (context == null) { context = new ParserContext(); }
            return ParsePage(analyseResult, ParserPoll.GetParser(parserType, context));
        }

        public static void ParseZip(ZipFile input, Stream output, Encoding encoding, ParserType type = ParserSetting.DefaultParserType)
        {
            var zipDirectorySeparator = Path.AltDirectorySeparatorChar.ToString();
            var htmlEntities = input.Where(o => o.IsDirectory == false && IsHtmlFile(o.FileName));
            if (htmlEntities.Count() == 0)
            {
                throw new Exception("Can not find any html file.");
            }

            // get base directory
            var refFile = htmlEntities.FirstOrDefault();
            var zipBaseDirectory = string.Empty;
            if (refFile != null)
            {
                zipBaseDirectory = Path.GetDirectoryName(refFile.FileName).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
                if (zipBaseDirectory.Length > 0)
                    zipBaseDirectory += zipDirectorySeparator;
            }

            // load kooboo cms site template
            ZipFile outputZip = null;
            if (!string.IsNullOrWhiteSpace(ParserSetting.SiteTemplatePath) &&
                File.Exists(ParserSetting.SiteTemplatePath))
            {
                outputZip = new ZipFile(ParserSetting.SiteTemplatePath, encoding);
            }
            else
            {
                var stream = RuntimeKit.LoadEmbeddedResource("TemplateSite.zip");
                outputZip = ZipFile.Read(stream);
            }

            // 1.analysis all pages
            var cachedAnalyseResults = new List<IAnalyseResult>();
            foreach (var entity in htmlEntities)
            {
                var ms = new MemoryStream();
                entity.OpenReader().CopyTo(ms);
                var htmlText = encoding.GetString(ms.ToArray());
                var fileName = Path.GetFileName(entity.FileName);
                cachedAnalyseResults.AddRange(AnalysePage(fileName, htmlText));
            }

            // 2.parse html file
            var cachedParseResults = new List<IParseResult>();
            var orderedScripts = new List<string>();
            var orderedStyles = new List<string>();
            var allSchemas = new List<Kooboo.CMS.Content.Models.Schema>();

            var context = new ParserContext();
            context.Items.Add(AllSchemasKey, allSchemas);
            context.Items.Add(CachedAnalyseResultsKey, cachedAnalyseResults);
            foreach (var analyseResult in cachedAnalyseResults)
            {
                // parse
                context.Items[CurrentAnalyseResultKey] = analyseResult;
                var parseResult = ParsePage(analyseResult, context, type);
                // merge
                MergeStringList(parseResult.OrderedScripts, orderedScripts);
                MergeStringList(parseResult.OrderedStyles, orderedStyles);
                // cache
                cachedParseResults.Add(parseResult);
            }
            // gain page releation
            foreach (var parsedResult in cachedParseResults)
            {
                var analyseResult = cachedAnalyseResults.Where(o => o.PageSetting.Name.IgnoreCaseEquals(parsedResult.Page.Name)).First();
                var parentName = analyseResult.PageSetting.Parent;
                if (!string.IsNullOrEmpty(parentName))
                    parsedResult.Page.Parent = cachedParseResults.Where(o => o.Page.Name.IgnoreCaseEquals(parentName)).Select(o => o.Page).First();
            }

            // 3.create repository
            var repository = CreateRepository(input, zipBaseDirectory, allSchemas);
            if (repository != null)
            {
                // pack
                var repositoryStream = new MemoryStream();
                var fullPath = Path.Combine(Kooboo.CMS.Content.Models.Paths.RepositoryPath.BasePhysicalPath, repository.Name);
                var repositoryZip = new Ionic.Zip.ZipFile(encoding);
                var repositoryFiles = Directory.GetFiles(fullPath, "*", SearchOption.AllDirectories);
                foreach (var file in repositoryFiles)
                    repositoryZip.AddEntry(file.Substring(fullPath.Length + 1), File.ReadAllBytes(file));
                repositoryZip.Save(repositoryStream);
                // write
                repositoryStream.Position = 0;
                outputZip.AddEntry(repository.Name + ".zip", repositoryStream);
                // remove
                try { Directory.Delete(fullPath, true); }
                catch (Exception) { }
                // write site setting
                var setting = outputZip.Entries.Where(o => "setting.config".IgnoreCaseEquals(o.FileName)).FirstOrDefault();
                if (setting != null)
                {
                    // load default setting
                    var settingStream = new MemoryStream();
                    setting.OpenReader().CopyTo(settingStream);
                    settingStream.Position = 0;
                    var serializer = new System.Runtime.Serialization.DataContractSerializer(typeof(Kooboo.CMS.Sites.Models.Site));
                    var site = (Kooboo.CMS.Sites.Models.Site)serializer.ReadObject(settingStream);
                    site.Repository = repository.Name;
                    site.Domains = null;
                    // write setting back
                    var newSettingStream = new MemoryStream();
                    serializer.WriteObject(newSettingStream, site);
                    newSettingStream.Position = 0;
                    outputZip.UpdateEntry(setting.FileName, newSettingStream);
                }
            }

            // 4.write views
            Action<string, object, IEnumerable<Type>> WriteSettingEntry = (fileDirectory, entry, knownTypes) =>
            {
                var settingStream = new MemoryStream();
                var serializer = new System.Runtime.Serialization.DataContractSerializer(entry.GetType(), knownTypes);
                serializer.WriteObject(settingStream, entry);
                settingStream.Position = 0;
                outputZip.AddEntry(ZipPathCombine(fileDirectory, "setting.config"), settingStream);
            };
            foreach (var parseResult in cachedParseResults)
            {
                var layout = parseResult.Layout;
                if (layout != null && cachedParseResults.Count(o => o.Page.Layout.IgnoreCaseEquals(layout.Name)) > 0)
                {
                    var fileDirectory = ZipPathCombine(string.Join(zipDirectorySeparator, layout.RelativePaths), layout.Name);
                    var layoutName = ZipPathCombine(fileDirectory, layout.TemplateFileName);
                    if (outputZip.Count(o => o.FileName.Contains(layoutName)) == 0)
                    {
                        outputZip.AddEntry(layoutName, layout.Body, encoding);
                        WriteSettingEntry(fileDirectory, layout, new Type[] { typeof(Kooboo.CMS.Sites.Models.Layout) });
                    }
                }

                var page = parseResult.Page;
                if (page != null)
                {
                    var fileDirectory = ZipPathCombine(string.Join(zipDirectorySeparator, page.RelativePaths), page.Name);
                    WriteSettingEntry(fileDirectory, page, new Type[] { typeof(Kooboo.CMS.Sites.Models.Page) });
                }

                foreach (var view in parseResult.Views)
                {
                    var fileDirectory = ZipPathCombine(string.Join(zipDirectorySeparator, view.RelativePaths), view.Name);
                    var viewName = ZipPathCombine(fileDirectory, view.TemplateFileName);
                    if (outputZip.Count(o => o.FileName.Contains(viewName)) == 0)
                    {
                        outputZip.AddEntry(viewName, view.Body, encoding);
                        WriteSettingEntry(fileDirectory, view, new Type[] { typeof(Kooboo.CMS.Sites.Models.View) });
                    }
                }

                foreach (var htmlBlock in parseResult.HtmlBlocks)
                {
                    var fileDirectory = ZipPathCombine(string.Join(zipDirectorySeparator, htmlBlock.RelativePaths), htmlBlock.Name);
                    var fileName = ZipPathCombine(fileDirectory, htmlBlock.DataFileName);
                    if (outputZip.Count(o => o.FileName.Contains(fileName)) == 0)
                    {
                        outputZip.AddEntry(fileName, htmlBlock.Body, encoding);
                    }
                }
            }

            // 5.copy other file resource
            var resources = input.Entries.Where(o => !o.IsDirectory && !IsHtmlFile(o.FileName));
            foreach (var entity in resources)
            {
                foreach (var map in DirectoryMap)
                {
                    var startPath = ZipPathCombine(zipBaseDirectory, map.From);
                    if (entity.FileName.IgnoreCaseStartsWith(startPath))
                    {
                        var ms = new MemoryStream();
                        entity.OpenReader().CopyTo(ms);
                        if (ms.Length > 0)
                        {
                            ms.Position = 0;
                            var fileName = entity.FileName.Substring(startPath.Length);
                            outputZip.AddEntry(ZipPathCombine(map.To + fileName), ms);
                        }

                        break;
                    }
                }
            }

            // 6.write order.txt
            if (orderedScripts.Count > 1)
            {
                var scriptOrderText = string.Join(Environment.NewLine, orderedScripts);
                outputZip.AddEntry("Scripts/Order.txt", scriptOrderText, encoding).Attributes = FileAttributes.Hidden;
            }
            if (orderedStyles.Count > 1)
            {
                var styleOrderText = string.Join(Environment.NewLine, orderedStyles);
                outputZip.AddEntry("Themes/Default/Order.txt", styleOrderText, encoding).Attributes = FileAttributes.Hidden;
            }

            // 7.copy assigned files
            var assignedFiles = new string[] { "preview.png", "Design.site" };
            foreach (var fileName in assignedFiles)
            {
                var assignedEntity = input.Entries.Where(o => o.FileName.IgnoreCaseStartsWith(ZipPathCombine(zipBaseDirectory, fileName))).FirstOrDefault();
                if (assignedEntity != null)
                    outputZip.AddEntry(fileName, assignedEntity.OpenReader());
            }

            // 8.ret
            outputZip.Save(output);
            outputZip.Dispose();
            //output.Position = 0;
        }

        public static void ParseZip(Stream input, Stream output, Encoding encoding, ParserType type = ParserSetting.DefaultParserType)
        {
            //input.Position = 0;
            ParseZip(ZipFile.Read(input), output, encoding, type);
        }

        public static void ParseZip(string input, Stream output, Encoding encoding, ParserType type = ParserSetting.DefaultParserType)
        {
            ParseZip(File.OpenRead(input), output, encoding, type);
        }

        private static bool IsHtmlFile(string fileName)
        {
            return fileName.IgnoreCaseEndsWith(".htm") || fileName.IgnoreCaseEndsWith(".html");
        }

        private static string ZipPathCombine(params string[] paths)
        {
            var zipDirectorySeparator = Path.AltDirectorySeparatorChar.ToString();
            var pathList = new List<string>();
            for (var i = 0; i < paths.Length; i++)
            {
                if (!string.IsNullOrEmpty(paths[i]))
                {
                    pathList.Add(paths[i].Trim(Path.AltDirectorySeparatorChar).Trim(Path.DirectorySeparatorChar));
                }
            }
            return string.Join(zipDirectorySeparator, pathList);
        }

        private static void MergeStringList(IList<string> mergeFrom, IList<string> mergeTo)
        {
            var loopOffset = 0;
            var previousOffset = 0;
            for (var i = 0; i < mergeFrom.Count; i++)
            {
                loopOffset++;
                var name = Path.GetFileName(mergeFrom[i]);
                var index = mergeTo.IndexOf(name);
                if (index != -1)
                {
                    loopOffset = 0; // reset
                    previousOffset = index;
                }
                else
                {
                    var insertIndex = previousOffset + loopOffset;
                    if (insertIndex >= mergeTo.Count)
                    {
                        mergeTo.Add(name);
                    }
                    else
                    {
                        mergeTo.Insert(insertIndex, name);
                    }
                }
            }
        }

        private static Kooboo.CMS.Content.Models.Repository CreateRepository(ZipFile input, string zipBaseDirectory, IList<Kooboo.CMS.Content.Models.Schema> schemas)
        {
            //create repository //saved..
            //create schema.. -> repository... //saved...
            //create textfolder. repository,schema. //saved..
            //create text content,  repository,schema,folder.

            // create repository
            var repositoryName = Guid.NewGuid().ToString().Replace("-", string.Empty);
            var repository = new Kooboo.CMS.Content.Models.Repository(repositoryName);
            repository.EnableCustomTemplate = true;
            //repository.EnableManuallyUserKey = true;
            var repositoryManager = Kooboo.CMS.Common.Runtime.EngineContext.Current.Resolve<Kooboo.CMS.Content.Services.RepositoryManager>();
            repositoryManager.Add(repository);

            foreach (var item in schemas)
            {
                // create schema
                var schemaName = item.Name;
                var schema = new Kooboo.CMS.Content.Models.Schema(repository, schemaName);
                foreach (var column in item.Columns)
                {
                    schema.Columns.Add(new Kooboo.CMS.Content.Models.Column()
                    {
                        Name = column.Name,
                        Label = column.Label,
                        ControlType = column.ControlType,
                        ShowInGrid = column.ShowInGrid,
                        DataType = column.DataType,
                        Order = column.Order,
                        DefaultValue = null,
                        Tooltip = null
                    });
                }
                var schemaManager = Kooboo.CMS.Common.Runtime.EngineContext.Current.Resolve<Kooboo.CMS.Content.Services.SchemaManager>();
                schemaManager.Add(repository, schema);
                schemaManager.ResetForm(repository, schemaName, Kooboo.CMS.Content.Models.FormType.Create |
                                                                Kooboo.CMS.Content.Models.FormType.Detail |
                                                                Kooboo.CMS.Content.Models.FormType.Grid |
                                                                Kooboo.CMS.Content.Models.FormType.List |
                                                                Kooboo.CMS.Content.Models.FormType.Selectable |
                                                                Kooboo.CMS.Content.Models.FormType.Update);

                // create textfolder
                var folderName = item.Name;
                var folder = new Kooboo.CMS.Content.Models.TextFolder(repository, folderName);
                folder.SchemaName = schemaName;
                var folderManager = Kooboo.CMS.Common.Runtime.EngineContext.Current.Resolve<Kooboo.CMS.Content.Services.TextFolderManager>();
                folderManager.Add(repository, folder);

                // create textcontent
                var contentManager = Kooboo.CMS.Common.Runtime.EngineContext.Current.Resolve<Kooboo.CMS.Content.Services.TextContentManager>();
                var values = new System.Collections.Specialized.NameValueCollection();
                values.Add("Published", "True");
                var files = new HttpFileCollectionCustomizable();
                foreach (var data in item.Columns)
                {
                    if (ParserSetting.ControlType_File.IgnoreCaseEquals(data.ControlType))
                    {
                        var filePath = ZipPathCombine(zipBaseDirectory, data.DefaultValue);
                        var fileEntity = input.Entries.Where(o => o.FileName.IgnoreCaseEquals(filePath)).FirstOrDefault();
                        if (fileEntity != null)
                        {
                            var stream = new MemoryStream();
                            fileEntity.OpenReader().CopyTo(stream);
                            if (stream.Length > 0)
                            {
                                var fileName = Path.GetFileName(filePath);
                                files.AddFile(data.Name, new HttpPostedFileCustomizable(fileName, stream));
                            }
                        }
                    }
                    else
                    {
                        values.Add(data.Name, data.DefaultValue);
                    }
                }
                for (var i = 0; i < ParserSetting.DefaultTextContentCount; i++)
                {
                    for (var k = 0; k < files.Count; k++)
                        files[k].InputStream.Position = 0;
                    contentManager.Add(repository, folder, values, files, null);
                }
            }

            // ret
            return repository;
        }
    }
}
